﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Futaba.SBus.Readers
{
	/// <summary>
	/// Implements <see cref="ISbusStreamReader"/> and reads data from a file
	/// </summary>
	public class FileSbusStreamReader : ISbusStreamReader, INotifyPropertyChanged
	{

		/// <summary>
		/// Raised, if any property in this instance has changed it's value
		/// </summary>
		/// <seealso cref="INotifyPropertyChanged"/>
		public event PropertyChangedEventHandler PropertyChanged;

		/// <summary>
		/// Creates a default instance of this class
		/// </summary>
		/// <param name="filename">the file to process</param>
		/// <param name="packetSize">the packet-size, used for honoring the <paramref name="packetsPerSecond"/></param>
		/// <param name="packetsPerSecond">the amount of <paramref name="packetSize">packets</paramref> to return per second</param>
		/// <param name="loop">wether to loop to the beginning of the stream, when the end has been reached</param>
		public FileSbusStreamReader(string filename, int packetSize = 24, int packetsPerSecond = 1, bool loop = false)
		{
			Filename = filename;
			PacketSize = packetSize;
			PacketsPerSecond = packetsPerSecond;
			Loop = loop;
		}

		private string _internalFilename;
		/// <summary>
		/// The file to process
		/// </summary>
		public string Filename
		{
			get { return _internalFilename; }
			protected set
			{
				if (Equals(_internalFilename, value))
					return;
				_internalFilename = value;
				RaisePropertyChanged(); //RaisePropertyChanged("Filename");
			}
		}

		private int _internalPauseDelay = 500;
		/// <summary>
		/// The delay with which a "non-read" value is returned, if <see cref="Pause"/>d
		/// (default = 500)
		/// </summary>
		public int PauseDelay
		{
			get { return _internalPauseDelay; }
			set
			{
				if (Equals(_internalPauseDelay, value))
					return;
				if (value < 0 || value > 1000)
					// ReSharper disable once NotResolvedInText
					throw new ArgumentOutOfRangeException("PauseDelay", "PauseDelay must be between 0 and 1000");
				_internalPauseDelay = value;
				RaisePropertyChanged(); //RaisePropertyChanged("PauseDelay");
			}
		}


		private bool _internalLoop;
		/// <summary>
		/// Wether to loop to the beginning of the stream, when the end has been reached
		/// </summary>
		public bool Loop
		{
			get { return _internalLoop; }
			set
			{
				if (Equals(_internalLoop, value))
					return;
				_internalLoop = value;
				RaisePropertyChanged(); //RaisePropertyChanged("Loop");
			}
		}

		private int _internalPacketSize;
		/// <summary>
		/// The packet-size, used for honoring the <see cref="PacketsPerSecond"/>
		/// </summary>
		/// <seealso cref="PacketsPerSecond"/>
		public int PacketSize
		{
			get { return _internalPacketSize; }
			set
			{
				if (Equals(_internalPacketSize, value))
					return;
				_internalPacketSize = value;
				RaisePropertyChanged(); //RaisePropertyChanged("PacketSize");
			}
		}

		private bool _internalPause;
		/// <summary>
		/// As long as this is true, <see cref="ReadByte"/> and <see cref="Peek"/>
		/// will return NULL
		/// </summary>
		public bool Pause
		{
			get { return _internalPause; }
			set
			{
				if (Equals(_internalPause, value))
					return;
				_internalPause = value;
				RaisePropertyChanged(); //RaisePropertyChanged("Pause");
			}
		}

		//private long _internalFileOffset;
		/// <summary>
		/// The current offset within the <see cref="Filename">file</see>
		/// </summary>
		public long FileOffset
		{
			get { return _sbusStream == null ? 0 : _sbusStream.Position; /* _internalFileOffset;*/ }
			// ReSharper disable once ValueParameterNotUsed
			private set
			{
				//if (Equals(_internalFileOffset, value))
				//	return;
				//_internalFileOffset = value;
				RaisePropertyChanged(); //RaisePropertyChanged("FileOffset");
			}
		}

		/// <summary>
		/// The amount of packets to return per second
		/// </summary>
		/// <seealso cref="PacketSize"/>
		public int PacketsPerSecond
		{
			get
			{
				return _packetsPerSecond;
			}
			set
			{
				_packetsPerSecond = value;
				_factor = value <= 0 ? 0 : 1D / PacketsPerSecond;
				RaisePropertyChanged(); //RaisePropertyChanged("PacketsPerSecond");
			}
		}

		/// <summary>
		/// Wether the file's end has been reached
		/// </summary>
		/// <seealso cref="Loop"/>
		public bool EndOfStream
		{
			get
			{
				//if (_sbusStream.CanSeek)
				//{
				bool atEnd = _sbusStream != null && (_sbusStream.Position >= _sbusStream.Length);
				return atEnd;
				//}
			}
		}

		private int _packetsPerSecond;
		private double _factor;
		private int _byteInPacket;
		private DateTime _packetStarted;
		private byte? _peekedByte;
		private Stream _sbusStream;

		/// <summary>
		/// Opens the underlying stream
		/// </summary>
		public void OpenPort()
		{
			_sbusStream = OpenFile();
		}

		/// <summary>
		/// Opens the <see cref="Filename">file</see>
		/// </summary>
		/// <returns>the stream to the file</returns>
		protected virtual Stream OpenFile()
		{
			if (string.IsNullOrEmpty(Filename))
				throw new InvalidOperationException("Set Filename first!");

			var file = new FileInfo(Filename);
			if (!file.Exists)
				throw new InvalidOperationException(string.Format("File '{0}' does not exist.", file.FullName));
			return file.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
		}

		/// <summary>
		/// Closes the underlying stream and releases all related resources
		/// </summary>
		public void ReleasePort()
		{
			if (_sbusStream == null)
				return;

			_sbusStream.Close();
			_sbusStream.Dispose();
			_sbusStream = null;
		}

		/// <summary>
		/// Returns the next byte waiting in the stream's buffer, or NULL, if no data is available
		/// </summary>
		/// <returns></returns>
		public byte? ReadByte()
		{
			if (_peekedByte.HasValue)
			{
				byte value = _peekedByte.Value;
				_peekedByte = null;
				return value;
			}
			int tempValue = DoRead();
			return tempValue != -1 ? (byte?)tempValue : null;
		}

		/// <summary>
		/// Returns the next byte waiting in the stream's buffer but will not remove it from the 
		/// buffer, or NULL, if no data is available
		/// </summary>
		/// <remarks>
		/// Multiple calls will always return the same byte value (if a non-null value has been returned
		/// on previous calls), until <see cref="ReadByte"/> has been called
		/// </remarks>
		public byte? Peek()
		{
			if (!_peekedByte.HasValue)
			{
				int tempValue = DoRead();
				_peekedByte = tempValue != -1 ? (byte?)tempValue : null;
			}

			return _peekedByte;
		}

		private int DoRead()
		{
			if (Pause)
			{
				Thread.Sleep(PauseDelay);
				return -1;
			}

			// ReSharper disable once CompareOfFloatsByEqualityOperator
			if (_factor != 0)
			{
				if (_byteInPacket >= PacketSize)
				{
					var diff = DateTime.Now.Subtract(_packetStarted).TotalSeconds;
					if (diff < _factor)
					{
						var delay = (int)Math.Floor((_factor - diff) * 1000);
#if DEBUG
						//Debug.WriteLine("Pausing for {2}ms, factor={1:0.00}, Last Read: {0:0.00}sec.",
						//diff, _factor, delay);
#endif
						Thread.Sleep(delay);
						//return -1;
					}
					_byteInPacket = 0;
				}
			}

			if (EndOfStream && Loop)
			{
#if DEBUG
				Debug.WriteLine("Reached end of stream. returning to beginning");
#endif
				FileOffset = 0;
				_sbusStream.Seek(0, SeekOrigin.Begin);
			}
			else if (EndOfStream)
				// ReSharper disable once ExplicitCallerInfoArgument
				RaisePropertyChanged("EndOfStream");

			int readByte = _sbusStream.ReadByte();
			if (readByte != -1)
			{
				FileOffset++;
				if (_byteInPacket == 0)
					_packetStarted = DateTime.Now;
				_byteInPacket++;
#if DEBUG
				//Debug.WriteLine("Byte {0}/{1}: {2}", _byteInPacket, PacketSize, readByte);
#endif
			}

			return readByte;
		}

		/// <summary>
		/// Works similar to <see cref="Peek"/>, but will consume the byte, if it matches the
		/// <paramref name="match">supplied value</paramref>
		/// </summary>
		/// <param name="match">the value to match against</param>
		/// <returns></returns>
		public bool ReadIfMatch(byte match)
		{
			var peek = Peek();
			if (!peek.HasValue || peek.Value != match)
				return false;

			ReadByte();
			return true;
		}

		/// <summary>
		/// Moves to the specified location in the <see cref="Filename">file</see>
		/// </summary>
		/// <param name="position">the position to move to</param>
		/// <param name="origin">how to interpret the <paramref name="position"/></param>
		public void Seek(int position, SeekOrigin origin)
		{
			FileOffset = _sbusStream.Seek(position, origin);
		}

		private void RaisePropertyChanged([CallerMemberName]string propertyName = null)
		{
			if (PropertyChanged == null)
				return;

			PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}
	}
}